package com.huahai.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache;
import com.huahai.api.aliyunai.model.CreateOutPaintingTaskRequest;
import com.huahai.api.aliyunai.model.CreateOutPaintingTaskResponse;
import com.huahai.api.aliyunai.sub.ALiYunAIApi;
import com.huahai.cofig.CosClientConfig;
import com.huahai.common.DeleteRequest;
import com.huahai.constant.CacheConstant;
import com.huahai.constant.RedisConstant;
import com.huahai.event.SpaceDeletedEvent;
import com.huahai.exception.BusinessException;
import com.huahai.exception.ErrorCode;
import com.huahai.manager.CosManager;
import com.huahai.manager.auth.StpKit;
import com.huahai.manager.auth.constants.SpaceUserPermissionConstant;
import com.huahai.manager.upload.FilePictureUpload;
import com.huahai.manager.upload.PictureUploadTemplate;
import com.huahai.manager.upload.UrlPictureUpload;
import com.huahai.mapper.PictureMapper;
import com.huahai.pojo.dto.file.UploadPictureDTO;
import com.huahai.pojo.dto.picture.*;
import com.huahai.pojo.entity.Picture;
import com.huahai.pojo.entity.Space;
import com.huahai.pojo.entity.User;
import com.huahai.pojo.enums.PictureReviewStatusEnums;
import com.huahai.pojo.vo.picture.PictureVO;
import com.huahai.pojo.vo.user.UserVO;
import com.huahai.service.PictureService;
import com.huahai.service.SpaceService;
import com.huahai.service.UserService;
import com.huahai.util.ColorSimilarUtils;
import com.huahai.util.ColorTransformUtils;
import com.huahai.util.ThrowUtils;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.beans.BeanUtils;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.*;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.DigestUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.awt.*;
import java.io.IOException;
import java.util.List;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author huahai
 * @description 针对表【picture(图片)】的数据库操作Service实现
 * @createDate 2025-07-19 20:39:17
 */
@Slf4j
@Service
public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture> implements PictureService {

//    @Resource
//    private FileManager fileManager;

    @Resource
    FilePictureUpload filePictureUpload;

    @Resource
    private UrlPictureUpload urlPictureUpload;

    @Resource
    private UserService userService;

    @Resource
    private SpaceService spaceService;

    @Resource
    private CosManager cosManager;

    @Resource
    private CosClientConfig cosClientConfig;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private ALiYunAIApi aLiYunAiApi;

    /**
     * 事务模板
     */
    @Resource
    private TransactionTemplate transactionTemplate;

    @Resource
    private Cache<String, String> localCache;

    private static final String SAMPLE_IMAGE = "示例图片";

    /**
     * 构建本地缓存
     */
    @Resource
    private Cache<String, String> LOCAL_CACHE;

    // 图片浏览次数缓存 key
    private final String PICTURE_VIEW_COUNT_KEY = RedisConstant.PROJECT_NAME + RedisConstant.VIEW_COUNT;

    // 图片点赞数缓存 key
    private final String PICTURE_LIKE_COUNT_KEY = RedisConstant.PROJECT_NAME + RedisConstant.LIKE_COUNT;

    // 图片点赞列表缓存 key
    private final String PICTURE_LIKE_LIST_KEY = RedisConstant.PROJECT_NAME + RedisConstant.LIKED_USER_LIST;

    /**
     * 图片上传
     *
     * @param inputResource    输入源
     * @param pictureUploadDTO 图片上传信息（图片的id）
     * @param loginUser        登录用户
     * @return 封装后的图片信息
     */
    @Override
    public PictureVO uploadPicture(Object inputResource, PictureUploadDTO pictureUploadDTO, User loginUser) {
        // 1. 校验参数
        ThrowUtils.throwIf(inputResource == null, ErrorCode.PARAMS_ERROR, "上传图片不能为空");
        ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR);
        // 校验空间
        Long spaceId = pictureUploadDTO.getSpaceId();
        if (spaceId != null) {
            // 校验空间是否为存在
            Space space = spaceService.getById(spaceId);
            ThrowUtils.throwIf(space == null, ErrorCode.PARAMS_ERROR, "空间不存在");
            // 已改为 Sa-Token 鉴权
//            // 校验当前登录用户是否有权限操作该空间
//            if (!loginUser.getId().equals(space.getUserId())) {
//                throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "您无权限操作此空间");
//            }
            if (space.getTotalCount() >= space.getMaxCount()) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "空间条数已满");
            }
            if (space.getTotalSize() >= space.getMaxSize()) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "空间大小已满");
            }
        }
        // 2. 判断当前上传图片的操作是更新还是新增（图片id） 更新操作才拿到图片id
        Long pictureId = null;
        if (pictureUploadDTO != null) {
            pictureId = pictureUploadDTO.getId();
        }
        // 3. 如果是更新操作，则判断图片是否存在
        Picture oldPicture;
        if (pictureId != null && pictureId > 0) {
//            boolean exists = this.lambdaQuery().eq(Picture::getId, pictureId).exists();
//            ThrowUtils.throwIf(!exists, ErrorCode.NOT_FOUND_ERROR, "图片不存在");
            // 判断当前登录用户是否有更新的权限
            oldPicture = this.getById(pictureId);
            ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);
            // 已改为 Sa-Token 鉴权
//            if (!oldPicture.getUserId().equals(loginUser.getId()) && !loginUser.getUserRole().equals(UserConstant.ADMIN_ROLE)) {
//                throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
//            }
//            // 存在表示更新操作，自动清理cos里的旧图片
//            this.cleanCosPicture(oldPicture);
            // 判断是否要复用原有图片的空间
            if (spaceId == null) {
                // 公共图库
                if (oldPicture.getSpaceId() != null) {
                    spaceId = oldPicture.getSpaceId();
                }
            } else {
                // 表示私有图库，必须与原图 space 一致
                if (ObjUtil.notEqual(spaceId, oldPicture.getSpaceId())) {
                    throw new BusinessException(ErrorCode.PARAMS_ERROR, "空间 id 不一致");
                }
            }
        } else {
            // 新增操作
            oldPicture = null;
        }
        // 4. 进行图片的上传
        // 4.1 用用户id构造图片上传路径，便于管理
        // => 修改为 公共图库 和 私有空间 按照空间划分目录
        String uploadPathPrefix;
        if (spaceId == null) {
            // 上传到公共图库
            uploadPathPrefix = String.format("public/%s", loginUser.getId());
        } else {
            // 上传到私有空间
            uploadPathPrefix = String.format("space/%s", spaceId);
        }

        // 4.2 上传图片的方法分类
        PictureUploadTemplate pictureUploadTemplate = filePictureUpload;
        if (inputResource instanceof String) {
            // 解析输入源，自动选择不同的上传方式
            pictureUploadTemplate = urlPictureUpload;
        }
        UploadPictureDTO uploadPictureDTO = pictureUploadTemplate.uploadPicture(inputResource, uploadPathPrefix);
        // 5. 将上传得到的结果进行解析，存入数据库
        // 5.1 封装入库的picture对象
        Picture picture = new Picture();
        picture.setUrl(uploadPictureDTO.getUrl());
        picture.setThumbnailUrl(uploadPictureDTO.getThumbnailUrl());
        // 在批量抓取图片时，如果前端传递的图片名称picName不为空，则使用picName作为图片名称存入数据库
        String picName = uploadPictureDTO.getPicName();
        if (pictureUploadDTO != null && StrUtil.isNotBlank(pictureUploadDTO.getPicName())) {
            picName = pictureUploadDTO.getPicName();
        }
        picture.setName(picName);
        // 在批量抓取图片时，如果前端传递的图片名称category不为空，则使用category作为图片名称存入数据库
        String category = uploadPictureDTO.getCategory();
        if (pictureUploadDTO != null && StrUtil.isNotBlank(pictureUploadDTO.getCategory())) {
            category = pictureUploadDTO.getCategory();
            picture.setCategory(category);
        }
        // 在批量抓取图片时，如果前端传递的图片名称tags不为空，则使用tags作为图片名称存入数据库
        List<String> tags = uploadPictureDTO.getTags();
        if (pictureUploadDTO != null && CollUtil.isNotEmpty(pictureUploadDTO.getTags())) {
            tags = pictureUploadDTO.getTags();
            // 注意将 list 转为 string
            picture.setTags(JSONUtil.toJsonStr(tags));
        }
        picture.setPicSize(uploadPictureDTO.getPicSize());
        picture.setPicWidth(uploadPictureDTO.getPicWidth());
        picture.setPicHeight(uploadPictureDTO.getPicHeight());
        picture.setPicScale(uploadPictureDTO.getPicScale());
        picture.setPicFormat(uploadPictureDTO.getPicFormat());
        // 将返回的图片颜色主色调保存到数据库中
//        picture.setPicColor(uploadPictureDTO.getPicColor());
        // 将颜色主色调补全转换后再保存到数据库中
        picture.setPicColor(ColorTransformUtils.colorTransform(uploadPictureDTO.getPicColor()));
        picture.setUserId(loginUser.getId());
        if (spaceId != null && spaceId > 0) {
            picture.setSpaceId(spaceId);
        }
        // 5.2 填补审核参数
        this.fillReviewParams(picture, loginUser);
        // 5.3 判断pictureId是否存在，存在则需要补充字段，不存在数据库会自动生成
        if (picture.getId() == null) {
            // 表示更新操作
            picture.setId(pictureId);
            picture.setEditTime(new Date());
        }
        // 5.4 开启事务
        Long finalSpaceId = spaceId;
        transactionTemplate.execute(status -> {
//            // 本项目默认不使用分库分表
//            // 补充空间 id，默认为 0 防止分库分表插入找不到指定的表
//            if (picture.getSpaceId() == null) {
//                picture.setSpaceId(0L);
//            }
            // 计算要更新的差值和数量变化
            long updateSize;
            int updateCount;
            if(oldPicture == null){
                // 新增操作
                updateSize = picture.getPicSize() != null ? picture.getPicSize() : 0;
                updateCount = 1;
            }else {
                // 更新操作
                // 计算更新操作的空间大小和数量变化
                long newSize = Optional.ofNullable(picture.getPicSize()).orElse(0L);
                long oldSize = Optional.ofNullable(oldPicture.getPicSize()).orElse(0L);
                updateSize = newSize - oldSize;
                updateCount = 0;
            }

            // 5.5 插入数据库图片数据
            boolean result = this.saveOrUpdate(picture);
            ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "图片上传失败，数据库异常");
            if (finalSpaceId != null ) {
                boolean updateResult = spaceService.lambdaUpdate().eq(Space::getId, finalSpaceId)
                        .setSql("totalSize = totalSize + " + updateSize)
                        .setSql("totalCount = totalCount + " + updateCount)
                        .update();
                ThrowUtils.throwIf(!updateResult, ErrorCode.OPERATION_ERROR, "图片额度更新失败");
            }
            // 新增成功，清理缓存
            this.cleanCaffeineAndRedisCache();
            return true;
        });
        // 6. 返回结果
        return PictureVO.objToVo(picture);
    }

    /**
     * 删除图片
     *
     * @param deleteRequest 删除参数
     * @param loginUser     登录用户
     * @return 删除结果
     */
    @Override
    public Boolean deletePicture(DeleteRequest deleteRequest, User loginUser) {
        // 1. 获取当前登录用户
        long id = deleteRequest.getId();
        // 2. 判断图片是否存在
        Picture oldPicture = this.getById(id);
        ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);
        // 已改为 Sa-Token 方式鉴权
        // // 3. 校验是否有删除的权限
        // this.checkPictureAuth(oldPicture, loginUser);
//        // 4. 删除cos里的压缩图和缩略图
//        this.cleanCosPicture(oldPicture);
        // 5 开启事务
        Long finalSpaceId = oldPicture.getSpaceId();
        transactionTemplate.execute(status -> {
//           // 指定主键 ID 和 spaceId ，防止分库分表导致找不到图片对应的空间
//           // 构造 QueryWrapper
//           QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
//           queryWrapper.eq("id", id)         // 指定主键 ID
//                   .eq("spaceId", oldPicture.getSpaceId()); // 附加 spaceId 条件
//           boolean result = this.remove(queryWrapper);
            // 5.1 操作数据库，执行删除操作
            boolean result = this.removeById(id);
            ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
            if (finalSpaceId != null) {
                boolean updateResult = spaceService.lambdaUpdate().eq(Space::getId, finalSpaceId)
                        .setSql("totalSize = totalSize - " + oldPicture.getPicSize())
                        .setSql("totalCount = totalCount - 1")
                        .update();
                ThrowUtils.throwIf(!updateResult, ErrorCode.OPERATION_ERROR, "图片额度更新失败");
            }
            // 删除成功，清理缓存
            this.cleanCaffeineAndRedisCache();
            return true;
        });
        // 7. 返回结果
        return true;
    }

    /**
     * 编辑图片（给用户使用）
     *
     * @param pictureEditDTO 图片编辑信息
     * @param loginUser      登录用户
     * @return 编辑结果
     */
    @Override
    public Boolean editPicture(PictureEditDTO pictureEditDTO, User loginUser) {
        // 1. 校验参数
        if (pictureEditDTO == null || pictureEditDTO.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // 2. 在此处将实体类和 DTO 进行转换
        Picture picture = new Picture();
        BeanUtils.copyProperties(pictureEditDTO, picture);
        // 注意将 list 转为 string
        picture.setTags(JSONUtil.toJsonStr(pictureEditDTO.getTags()));
        // 3. 设置编辑时间
        picture.setEditTime(new Date());
        // 4. 填补审核参数
        this.fillReviewParams(picture, loginUser);
        // 5. 数据校验
        this.validPicture(picture);
        // 6. 判断当前图片是否存在
        long id = pictureEditDTO.getId();
        Picture oldPicture = this.getById(id);
        ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);
        // 已改为 Sa-Token 方式鉴权
        // 7. 校验当前登录用户是否有编辑的权限
        // this.checkPictureAuth(oldPicture, loginUser);
        // 判断是否更改了空间中的图片
        Long spaceId = picture.getSpaceId();
        if (spaceId != null) {
            Space space = spaceService.getById(spaceId);
            Long newSize = picture.getPicSize() == null ? 0 : picture.getPicSize();
            Long oldSize = oldPicture.getPicSize() == null ? 0 : oldPicture.getPicSize();
            long sizeDiff = newSize - oldSize;
            // 有权限，则修改用户私有空间大小，减去之前老图片的大小
            boolean updateResult = spaceService.lambdaUpdate().eq(Space::getId, spaceId)
                    .setSql("totalSize = totalSize + " + sizeDiff)
                    .update();
            ThrowUtils.throwIf(!updateResult, ErrorCode.OPERATION_ERROR, "图片额度更新失败");
        }
        // 8. 操作数据库，执行修改操作
        boolean result = this.updateById(picture);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        // 9. 编辑成功，清理缓存
        this.cleanCaffeineAndRedisCache();
        // 10. 返回结果
        return true;
    }

    /**
     * 分页获取图片列表（封装类）利用多级缓存提升性能 (当前使用，对于私人空间可能没用必要使用)
     *
     * @param pictureQueryDTO 图片查询参数
     * @param request         请求
     * @return 分页后的脱敏图片列表
     */
    @Override
    public Page<PictureVO> listPictureVOByPageByCache(PictureQueryDTO pictureQueryDTO, HttpServletRequest request) {
        // 1. 获取分页参数
        long current = pictureQueryDTO.getCurrent();
        long size = pictureQueryDTO.getPageSize();
        // 2. 定义一次性最大获取图片数量，限制爬虫
        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR, "获取图片数量超过限制！");
        // 3. 判断空间权限校验
        Long spaceId = pictureQueryDTO.getSpaceId();
        if (spaceId == null) {
            // 表示公共图库
            // 3.1 过滤未审核通过的图片
            pictureQueryDTO.setReviewStatus(PictureReviewStatusEnums.PASS.getValue());
            // 只查询spaceId为空的图片
            pictureQueryDTO.setNullSpaceId(true);
        } else {
            // 表示私有图库
            // 改为 Sa-Token 编程式鉴权
            boolean hasPermission = StpKit.SPACE.hasPermission(SpaceUserPermissionConstant.PICTURE_VIEW);
            ThrowUtils.throwIf(!hasPermission, ErrorCode.NO_AUTH_ERROR);
//            // 3.2 校验当前登录用户权限
//            Space space = spaceService.getById(spaceId);
//            ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
//            User loginUser = userService.getLoginUser(request);
//            if (!loginUser.getId().equals(space.getUserId())) {
//                throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "您无权限操作此空间");
//            }
        }
        User loginUser = null;
        try {
            // 获取登录用户
            loginUser = userService.getLoginUser(request);
        } catch (Exception ignored) {}
        // 4. 构建本地缓存，查询本地缓存里是否已经写入缓存
        // 构建 key
        String jsonStr = JSONUtil.toJsonStr(pictureQueryDTO);
        // md5加密 key
        String md5JsonStr = DigestUtils.md5DigestAsHex(jsonStr.getBytes());
        String caffeineKey = "listPictureVOByPage:" + md5JsonStr;
        String jsonCaffeine = LOCAL_CACHE.getIfPresent(caffeineKey);
        if (jsonCaffeine != null) {
            // 查询成功，返回
            // 本地缓存读取
            JSONObject jsonObject = JSONUtil.parseObj(jsonCaffeine);
            Page<PictureVO> pictureVOPage = jsonObject.toBean(new TypeReference<Page<PictureVO>>() {});
            // 优先从 redis 缓存中获取图片的浏览量（本地缓存里不一定更新了浏览量）
            fillPictureInfoFromRedis(pictureVOPage, loginUser);
            return pictureVOPage;
        }

        // 5. 查询redis里是否已经写入该次查询的缓存
        String redisKey = RedisConstant.PROJECT_NAME + "listPictureVOByPage:" + md5JsonStr;
        // 5.3 查询redis
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        String pictureCache = opsForValue.get(redisKey);
        if (pictureCache != null) {
            // 查询成功，返回
            // Redis 缓存读取
            JSONObject jsonObject = JSONUtil.parseObj(pictureCache);
            Page<PictureVO> pictureVOPage = jsonObject.toBean(new TypeReference<Page<PictureVO>>() {});
            fillPictureInfoFromRedis(pictureVOPage, loginUser);
            return pictureVOPage;
        }
        // 6. 查询数据库
        Page<Picture> picturePage = this.page(new Page<>(current, size), this.getQueryWrapper(pictureQueryDTO));
        Page<PictureVO> pictureVoPage = this.getPictureVoPage(picturePage);
        fillPictureInfoFromRedis(pictureVoPage, loginUser);
        // 7. 刷新本地缓存
        LOCAL_CACHE.put(caffeineKey, JSONUtil.toJsonStr(pictureVoPage));

        // 8. 将查询的结果写入redis缓存
        // 8.1 构建随机过期时间为5 - 10 分钟，反正key在同一时间过期导致缓存雪崩
        int cacheExpireTime = 300 + RandomUtil.randomInt(0, 300);
        String jsonCachePictureVO = JSONUtil.toJsonStr(pictureVoPage);
        opsForValue.set(redisKey, jsonCachePictureVO, cacheExpireTime, TimeUnit.SECONDS);

        return pictureVoPage;
    }

    /**
     * 填充图片信息从 Redis 缓存（浏览量、点赞数、是否点赞）
     *
     * @param page      图片分页数据，支持 Picture 或 PictureVO 类型
     * @param loginUser 当前登录用户，用于判断是否点赞
     */
    public <T> void fillPictureInfoFromRedis(Page<T> page, User loginUser) {
        if(loginUser == null){
            return;
        }
        // 遍历分页中的每条图片记录，填充缓存中的浏览量、点赞数和是否点赞状态
        for (T record : page.getRecords()) {
            Long id;
            // 获取图片 ID，兼容 Picture 和 PictureVO 两种类型
            if (record instanceof Picture) {
                id = ((Picture) record).getId();
            } else if (record instanceof PictureVO) {
                id = ((PictureVO) record).getId();
            } else {
                throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的图片类型");
            }
            // 从 Redis 中获取浏览量并设置到记录中
            String viewCountStr = stringRedisTemplate.opsForValue().get(PICTURE_VIEW_COUNT_KEY + id);
            if (StrUtil.isNotBlank(viewCountStr)) {
                long viewCount = Long.parseLong(viewCountStr);
                if (record instanceof Picture) {
                    ((Picture) record).setViewCount(viewCount);
                } else if (record instanceof PictureVO) {
                    ((PictureVO) record).setViewCount(viewCount);
                }
            }
            // 从 Redis 中获取点赞数并设置到记录中
            String likeCountStr = stringRedisTemplate.opsForValue().get(PICTURE_LIKE_COUNT_KEY + id);
            if (StrUtil.isNotBlank(likeCountStr)) {
                long likeCount = Long.parseLong(likeCountStr);
                if (record instanceof Picture) {
                    ((Picture) record).setLikeCount(likeCount);
                } else if (record instanceof PictureVO) {
                    ((PictureVO) record).setLikeCount(likeCount);
                }
            }
            // 判断当前用户是否点赞该图片
            Boolean isLike = checkPictureLiked(
                    record instanceof PictureVO ? (PictureVO) record : PictureVO.objToVo((Picture) record),
                    loginUser
            );
            // 设置是否点赞状态
            if (record instanceof Picture) {
                ((Picture) record).setIsLike(isLike);
            } else if (record instanceof PictureVO) {
                ((PictureVO) record).setIsLike(isLike);
            }
        }
    }



    /**
     * 图片审核功能
     *
     * @param pictureReviewDTO 图片审核信息
     * @param loginUser        登录用户
     */
    @Override
    public void doReviewPicture(PictureReviewDTO pictureReviewDTO, User loginUser) {
        // 1. 校验参数
        ThrowUtils.throwIf(pictureReviewDTO == null, ErrorCode.PARAMS_ERROR);
        String reviewMessage = pictureReviewDTO.getReviewMessage();
        Long pictureId = pictureReviewDTO.getId();
        Integer reviewStatus = pictureReviewDTO.getReviewStatus();
        PictureReviewStatusEnums pictureReviewStatusEnums = PictureReviewStatusEnums.getEnumsByValue(reviewStatus);
        if (pictureId == null || pictureId <= 0
                || reviewStatus == null || PictureReviewStatusEnums.REVIEWING.equals(pictureReviewStatusEnums)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // 2. 判断图片是否存在
        Picture oldPicture = this.getById(pictureId);
        if (oldPicture == null) {
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
        }
        // 3. 判断是否重复设置审核状态
        if (oldPicture.getReviewStatus().equals(reviewStatus)) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "请勿重复设置审核状态");
        }
        // 4. 设置审核填充的信息
        Picture picture = new Picture();
        BeanUtils.copyProperties(pictureReviewDTO, picture);
        picture.setReviewerId(loginUser.getId());
        picture.setReviewTime(new Date());
        // 5. 修改数据库图片数据
        boolean updateResult = this.updateById(picture);
        // 审核成功，清理缓存
        this.cleanCaffeineAndRedisCache();
        ThrowUtils.throwIf(!updateResult, ErrorCode.OPERATION_ERROR, "图片审核失败，数据库异常");
    }

    /**
     * 批量抓取和创建图片
     *
     * @param pictureUploadByBatchDTO 图片批量上传信息
     * @param loginUser               登录用户
     * @return 成功创建的图片数
     */
    @Override
    public Integer uploadPictureByBatch(PictureUploadByBatchDTO pictureUploadByBatchDTO, User loginUser) {
        // 1. 校验参数
        String category = pictureUploadByBatchDTO.getCategory();
        List<String> tags = pictureUploadByBatchDTO.getTags();
        Integer count = pictureUploadByBatchDTO.getCount();
        if (count <= 0 || count > 30) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "抓取数量不能大于30条");
        }
        String searchText = pictureUploadByBatchDTO.getSearchText();
        // 为创建图片命名的前缀，允许前端传递创建图片的名称
        String namePrefix = pictureUploadByBatchDTO.getNamePrefix();
        if (StrUtil.isNotBlank(namePrefix)) {
            searchText = namePrefix;
        }
        // 2. 抓取图片html
        Document document;
        try {
            // 抓取网页地址
            String fetchUrl = String.format("https://cn.bing.com/images/async?q=%s&mmasync=1", searchText);
            // 解析抓取的 html
            document = Jsoup.connect(fetchUrl).get();
        } catch (IOException e) {
            log.error("获取页面失败", e);
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "获取页面失败");
        }
        // 3. 解析 html
        // 3.1 获取html的最外层div
        Element div = document.getElementsByClass("dgControl").first();
        if (ObjUtil.isNull(div)) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "获取图片失败");
        }
        // 3.2 获取div下每一张图片的img
        // 修改：抓取图片的高清大图，抓取data-m 属性中的 murl
//      Elements imgElementList = div.select("img.mimg");
        Elements imgElementList = div.select(".iusc");
        // 3.3 记录创建成功的图片数量
        int uploadSuccessCount = 0;
        // 3.4 遍历图片列表
        for (Element imgElement : imgElementList) {
            // 抓取图片的地址
//          String fileUrl = imgElement.attr("src");
            // 获取data-m属性中的JSON字符串
            String dataM = imgElement.attr("m");
            String fileUrl;
            try {
                // 解析json字符串
                JSONObject jsonObject = JSONUtil.parseObj(dataM);
                // 获取murl字段（原始图片的URL）
                fileUrl = jsonObject.getStr("murl");
            } catch (Exception e) {
                log.error("解析data-m属性失败", e);
                continue;
            }
            if (StrUtil.isBlank(fileUrl)) {
                log.info("当前链接为空，已跳过: {}", fileUrl);
                continue;
            }
            // 处理图片上传地址，防止出现转义问题
            int questionMarkIndex = fileUrl.indexOf("?");
            if (questionMarkIndex > -1) {
                fileUrl = fileUrl.substring(0, questionMarkIndex);
            }
            // 4. 上传到对象存储,存储到数据库
            PictureUploadDTO pictureUploadDTO = new PictureUploadDTO();
            // 补充图片名称
            if (StrUtil.isNotBlank(namePrefix)) {
                pictureUploadDTO.setPicName(namePrefix + SAMPLE_IMAGE + (uploadSuccessCount + 1));
            }
            // 补充图片分类
            if (StrUtil.isNotBlank(category)) {
                pictureUploadDTO.setCategory(category);
            }
            // 补充图片标签
            if (CollUtil.isNotEmpty(tags)) {
                pictureUploadDTO.setTags(tags);
            }
            try {
                PictureVO pictureVO = this.uploadPicture(fileUrl, pictureUploadDTO, loginUser);
                log.info("图片上传成功: {}", pictureVO.getId());
                uploadSuccessCount++;
            } catch (Exception e) {
                log.error("图片上传失败: {}", fileUrl);
                continue;
            }
            if (uploadSuccessCount >= count) {
                break;
            }
        }
        // 新增成功，清理缓存
        this.cleanCaffeineAndRedisCache();
        // 5. 返回创建成功的图片数量
        return uploadSuccessCount;
    }

    /**
     * 根据颜色查询排序空间图片
     *
     * @param searchPictureByColorDTO 颜色查询请求类
     * @param loginUser               登录用户
     * @return 排序后的图片列表
     */
    @Override
    public List<PictureVO> searchPictureByColor(SearchPictureByColorDTO searchPictureByColorDTO, User loginUser) {
        // 1. 校验参数
        Long spaceId = searchPictureByColorDTO.getSpaceId();
        if (spaceId == null || spaceId <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "空间id不存在");
        }
        ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR);
        // 2. 校验用户权限
        Space space = spaceService.getById(spaceId);
        ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
        if (!space.getUserId().equals(loginUser.getId())) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "您无权限操作此空间");
        }
        // 3. 查询空间下的所有图片，必须有主色调
        List<Picture> pictureList = this.lambdaQuery().eq(Picture::getSpaceId, spaceId).isNotNull(Picture::getPicColor).list();
        // 3.1 筛选出有主色调的图片,没用则返回空数组
        if (CollUtil.isEmpty(pictureList)) {
            return new ArrayList<>();
        }
        // 4. 根据欧几里得距离公式计算颜色之间的距离相似度
        // 4.1 将目标颜色转换为 color 对象
        Color targetColor = Color.decode(searchPictureByColorDTO.getPicColor());
        // 4.2 计算所有图片与目标颜色之间的距离
        List<Picture> sortedPictureList = pictureList.stream().sorted(Comparator.comparingDouble(picture -> {
            // 提取主色调 (默认为hex类型的色调)
            String hexColor = picture.getPicColor();
            if (StrUtil.isBlank(hexColor)) {
                // 如果不存在，默认返回最大值，靠最后排序
                return Double.MAX_VALUE;
            }
            Color pictureColor = Color.decode(hexColor);
            // 计算颜色之间的距离 (用负号表示距离越大的排越后面)
            return -ColorSimilarUtils.calculateSimilarity(pictureColor, targetColor);
        })).limit(20).collect(Collectors.toList());
        // 5. 返回排序后的脱敏图片列表
        return sortedPictureList.stream().map(PictureVO::objToVo).collect(Collectors.toList());
    }

    /**
     * 批量修改图片信息
     *
     * @param pictureEditByBatchDTO 图片批量修改信息
     * @param loginUser             登录用户
     * @return 修改成功的图片数
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public Boolean editPictureByBatch(PictureEditByBatchDTO pictureEditByBatchDTO, User loginUser) {
        // 1. 校验请求参数
        List<Long> pictureIdList = pictureEditByBatchDTO.getPictureIdList();
        Long spaceId = pictureEditByBatchDTO.getSpaceId();
        ThrowUtils.throwIf(CollUtil.isEmpty(pictureIdList), ErrorCode.PARAMS_ERROR);
        ThrowUtils.throwIf(spaceId == null || spaceId <= 0, ErrorCode.PARAMS_ERROR);
        String category = pictureEditByBatchDTO.getCategory();
        List<String> tags = pictureEditByBatchDTO.getTags();
        String nameRule = pictureEditByBatchDTO.getNameRule();
        // 2. 校验用户空间权限
        Space space = spaceService.getById(spaceId);
        ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
        if (!space.getUserId().equals(loginUser.getId())) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "您无操作该空间权限");
        }
        // 3. 查询指定图片（重点： 只查询需要的字段）
        List<Picture> pictureList = this.lambdaQuery()
                .select(Picture::getId, Picture::getSpaceId)
                .eq(Picture::getSpaceId, spaceId)
                .in(Picture::getId, pictureIdList)
                .list();
        if (CollUtil.isEmpty(pictureList)) {
            return false;
        }
        // 4. 批量更新图片的标签和分类和名字
        for (Picture picture : pictureList) {
            // 更新图片的标签和分类
            if (StrUtil.isNotBlank(category)) {
                picture.setCategory(category);
            }
            if (CollUtil.isNotEmpty(tags)) {
                // 批量更新图片的标签 (JSON转换后)
                picture.setTags(JSONUtil.toJsonStr(tags));
            }
        }
        // 批量更新图片的名字（这里指定用户指定的修改后加序号）
        fillPictureNameWithNameRule(pictureList, nameRule);
        // 5. 指定数据库，执行批量更新操作
        boolean result = this.updateBatchById(pictureList);
        if (!result) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "批量更新图片失败");
        }
        // 6. 返回修改的结果
        return true;
    }

    /**
     * 创建图片 AI 扩图任务
     *
     * @param createPictureOutPaintingTaskDTO 创建图片 AI 扩图任务信息
     * @param loginUser                       登录用户
     * @return 创建成功任务信息
     */
    @Override
    public CreateOutPaintingTaskResponse createOutPaintingTask(CreatePictureOutPaintingTaskDTO createPictureOutPaintingTaskDTO, User loginUser) {
        // 1. 校验参数
        ThrowUtils.throwIf(createPictureOutPaintingTaskDTO == null, ErrorCode.PARAMS_ERROR);
        Long pictureId = createPictureOutPaintingTaskDTO.getPictureId();
        ThrowUtils.throwIf(pictureId == null || pictureId <= 0, ErrorCode.PARAMS_ERROR, "图片 id 不存在");
        CreateOutPaintingTaskRequest.Parameters parameters = createPictureOutPaintingTaskDTO.getParameters();
        // 2. 获取图片信息
        Picture picture = this.getById(pictureId);
        ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR, "图片不存在");
        // 已改为 Sa-Token 方式鉴权
        // 3. 校验图片权限
//        checkPictureAuth(picture, loginUser);
        // 4. 封装创建扩图任务的请求参数
        CreateOutPaintingTaskRequest createOutPaintingTaskRequest = new CreateOutPaintingTaskRequest();
        CreateOutPaintingTaskRequest.Input input = new CreateOutPaintingTaskRequest.Input();
        // 获取图片的 url
        String picUrl = picture.getUrl();
        input.setImageUrl(picUrl);
        createOutPaintingTaskRequest.setInput(input);
        BeanUtils.copyProperties(createPictureOutPaintingTaskDTO, createOutPaintingTaskRequest);
        // 5. 调用创建任务的接口,返回创建成功的任务信息
        return aLiYunAiApi.createOutPaintingTask(createOutPaintingTaskRequest);
    }

    /**
     * 批量修改图片的名字 填充修改后的名字 名字{序号}
     *
     * @param pictureList 更新的图片列表
     * @param nameRule    修改后的名字规则
     */
    private void fillPictureNameWithNameRule(List<Picture> pictureList, String nameRule) {
        if (CollUtil.isEmpty(pictureList) || StrUtil.isBlank(nameRule)) {
            return;
        }
        // 定义修改序号
        int count = 1;
        try {
            for (Picture picture : pictureList) {
                // 替换序号
                String picName = nameRule.replaceAll("\\{序号}", String.valueOf(count++));
                picture.setName(picName);
            }
        } catch (Exception e) {
            log.error("图片名称转换异常", e);
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "图片名称解析错误");
        }
    }

    /**
     * 检查图片是否被当前用户点赞
     * @param pictureVO 图片信息
     * @param loginUser 登录用户
     * @return 是否被当前用户点赞
     */
    public Boolean checkPictureLiked(PictureVO pictureVO, User loginUser) {
        // 从 redis 缓存中获取图片的点赞列表
        return stringRedisTemplate.opsForSet()
                .isMember(PICTURE_LIKE_LIST_KEY + pictureVO.getId(), loginUser.getId().toString());
    }

    /**
     * picture类型转换脱敏（单张图片）
     *
     * @param picture 图片对象
     * @param request 请求
     * @return PictureVO
     */
    @Override
    public PictureVO getPictureVO(Picture picture, HttpServletRequest request) {
        // 对象转封装类
        PictureVO pictureVO = PictureVO.objToVo(picture);
        // 关联查询用户信息
        Long userId = picture.getUserId();
        if (userId != null && userId > 0) {
            User user = userService.getById(userId);
            UserVO userVO = userService.getUserVO(user);
            pictureVO.setUser(userVO);
        }
        return pictureVO;
    }

    /**
     * 分页获取图片封装
     *
     * @param picturePage 分页参数
     * @return Page<PictureVO> 分页后的脱敏图片列表
     */
    @Override
    public Page<PictureVO> getPictureVoPage(Page<Picture> picturePage) {
        List<Picture> pictureList = picturePage.getRecords();
        Page<PictureVO> pictureVoPage = new Page<>(picturePage.getCurrent(), picturePage.getSize(), picturePage.getTotal());
        if (CollUtil.isEmpty(pictureList)) {
            return pictureVoPage;
        }
        // 对象列表 => 封装对象列表
        List<PictureVO> pictureVOList = pictureList.stream().map(PictureVO::objToVo).collect(Collectors.toList());
        // 1. 关联查询用户信息
        Set<Long> userIdSet = pictureList.stream().map(Picture::getUserId).collect(Collectors.toSet());
        Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()
                .collect(Collectors.groupingBy(User::getId));
        // 2. 填充信息
        pictureVOList.forEach(pictureVO -> {
            Long userId = pictureVO.getUserId();
            User user = null;
            if (userIdUserListMap.containsKey(userId)) {
                user = userIdUserListMap.get(userId).get(0);
            }
            pictureVO.setUser(userService.getUserVO(user));
        });
        pictureVoPage.setRecords(pictureVOList);
        // 3. 返回数据
        return pictureVoPage;
    }


    /**
     * 封装图片查询条件QueryWrapper
     *
     * @param pictureQueryDTO 图片查询条件
     * @return QueryWrapper<Picture>
     */
    @Override
    public QueryWrapper<Picture> getQueryWrapper(PictureQueryDTO pictureQueryDTO) {
        QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
        if (pictureQueryDTO == null) {
            return queryWrapper;
        }
        ;

        // 从对象中取值
        Long id = pictureQueryDTO.getId();
        String name = pictureQueryDTO.getName();
        String introduction = pictureQueryDTO.getIntroduction();
        String category = pictureQueryDTO.getCategory();
        List<String> tags = pictureQueryDTO.getTags();
        Long picSize = pictureQueryDTO.getPicSize();
        Integer picWidth = pictureQueryDTO.getPicWidth();
        Integer picHeight = pictureQueryDTO.getPicHeight();
        Double picScale = pictureQueryDTO.getPicScale();
        String picFormat = pictureQueryDTO.getPicFormat();
        String searchText = pictureQueryDTO.getSearchText();
        // 查询点赞数和浏览量
        Long viewCount = pictureQueryDTO.getViewCount();
        Long likeCount = pictureQueryDTO.getLikeCount();
        Long commentCount = pictureQueryDTO.getCommentCount();

        Long userId = pictureQueryDTO.getUserId();
        Long spaceId = pictureQueryDTO.getSpaceId();
        // 判断是否只查询spaceId为空的图片
        Boolean nullSpaceId = pictureQueryDTO.getNullSpaceId();
        String sortField = pictureQueryDTO.getSortField();
        String sortOrder = pictureQueryDTO.getSortOrder();
        Integer reviewStatus = pictureQueryDTO.getReviewStatus();
        String reviewMessage = pictureQueryDTO.getReviewMessage();
        Long reviewerId = pictureQueryDTO.getReviewerId();
        // 添加编辑时间搜索
        Date startEditTime = pictureQueryDTO.getStartEditTime();
        Date endEditTime = pictureQueryDTO.getEndEditTime();
        // 从多字段中搜索
        if (StrUtil.isNotBlank(searchText)) {
            // 需要拼接查询条件
            queryWrapper.and(qw -> qw.like("name", searchText)
                    .or()
                    .like("introduction", searchText)
            );
        }
        queryWrapper.eq(ObjUtil.isNotEmpty(id), "id", id);
        queryWrapper.eq(ObjUtil.isNotEmpty(userId), "userId", userId);
        queryWrapper.eq(ObjUtil.isNotEmpty(spaceId), "spaceId", spaceId);
        // 判断是否只查询spaceId为空的图片
        queryWrapper.isNull(nullSpaceId, "spaceId");
        queryWrapper.like(StrUtil.isNotBlank(name), "name", name);
        queryWrapper.like(StrUtil.isNotBlank(introduction), "introduction", introduction);
        queryWrapper.like(StrUtil.isNotBlank(picFormat), "picFormat", picFormat);
        queryWrapper.eq(StrUtil.isNotBlank(category), "category", category);
        queryWrapper.eq(ObjUtil.isNotEmpty(picWidth), "picWidth", picWidth);
        queryWrapper.eq(ObjUtil.isNotEmpty(picHeight), "picHeight", picHeight);
        queryWrapper.eq(ObjUtil.isNotEmpty(picSize), "picSize", picSize);
        queryWrapper.eq(ObjUtil.isNotEmpty(picScale), "picScale", picScale);
        queryWrapper.eq(ObjUtil.isNotEmpty(viewCount), "viewCount", viewCount);
        queryWrapper.eq(ObjUtil.isNotEmpty(likeCount), "likeCount", likeCount);
        queryWrapper.eq(ObjUtil.isNotEmpty(commentCount), "commentCount", commentCount);
        queryWrapper.eq(ObjUtil.isNotEmpty(reviewStatus), "reviewStatus", reviewStatus);
        queryWrapper.like(StrUtil.isNotBlank(reviewMessage), "reviewMessage", reviewMessage);
        queryWrapper.eq(ObjUtil.isNotEmpty(reviewerId), "reviewerId", reviewerId);
        // startEditTime <= time < endEditTime
        queryWrapper.ge(ObjUtil.isNotEmpty(startEditTime), "editTime", startEditTime);
        queryWrapper.lt(ObjUtil.isNotEmpty(endEditTime), "editTime", endEditTime);
        // JSON 数组查询
        if (CollUtil.isNotEmpty(tags)) {
            for (String tag : tags) {
                queryWrapper.like("tags", "\"" + tag + "\"");
            }
        }
        // 排序
        queryWrapper.orderBy(StrUtil.isNotEmpty(sortField), sortOrder.equals("ascend"), sortField);
        return queryWrapper;
    }

    /**
     * 图片基本校验
     *
     * @param picture 图片对象
     */
    @Override
    public void validPicture(Picture picture) {
        ThrowUtils.throwIf(picture == null, ErrorCode.PARAMS_ERROR);
        // 从对象中取值
        Long id = picture.getId();
        String url = picture.getUrl();
        String introduction = picture.getIntroduction();
        // 修改数据时，id 不能为空，有参数则校验
        ThrowUtils.throwIf(ObjUtil.isNull(id), ErrorCode.PARAMS_ERROR, "id 不能为空");
        if (StrUtil.isNotBlank(url)) {
            ThrowUtils.throwIf(url.length() > 1024, ErrorCode.PARAMS_ERROR, "url 过长");
        }
        if (StrUtil.isNotBlank(introduction)) {
            ThrowUtils.throwIf(introduction.length() > 800, ErrorCode.PARAMS_ERROR, "简介过长");
        }
    }

    /**
     * 填充审核参数
     *
     * @param picture   图片对象
     * @param loginUser 登录用户
     */
    @Override
    public void fillReviewParams(Picture picture, User loginUser) {
        if (userService.isAdmin(loginUser)) {
            // 管理员自动过审
            picture.setReviewStatus(PictureReviewStatusEnums.PASS.getValue());
            picture.setReviewerId(loginUser.getId());
            picture.setReviewMessage("管理员自动过审");
            picture.setReviewTime(new Date());
        } else {
            // 非管理员，创建或编辑都要改为待审核
            picture.setReviewStatus(PictureReviewStatusEnums.REVIEWING.getValue());
        }
    }

    /**
     * 清除 COS 图片
     * //暂时没有必要删除，留做备份，之后强行修改数据库时可以找到图片
     *
     * @param oldPicture 旧图片对象
     */
    @Async // 删除图片操作后台异步执行
    @Override
    public void cleanCosPicture(Picture oldPicture) {
        // 1. 判断当前图片是否被多个记录使用
        String imgUrl = oldPicture.getUrl();
        Long count = this.lambdaQuery().eq(Picture::getUrl, imgUrl).count();
        // 2. 如果被多个记录使用，则不删除
        if (count > 1) {
            return;
        }
        // 3. 删除压缩图
        String cosHost = cosClientConfig.getHost();

        String cosUrl = this.changeUrlToCosKey(imgUrl, cosHost);
        cosManager.deleteImage(cosUrl);
        String thumbnailUrl = oldPicture.getThumbnailUrl();
        if (StrUtil.isNotBlank(thumbnailUrl)) {
            // 4. 删除缩略图
            String cosThumbnailUrl = this.changeUrlToCosKey(thumbnailUrl, cosHost);
            cosManager.deleteImage(cosThumbnailUrl);
        }
    }

    /**
     * 清除 caffeine 和 redis 的缓存（Caffeine+Redis）
     */
    @Override
    public void cleanCaffeineAndRedisCache() {
        // 如有需要清理其他key，可在该方法中添加key

        // 1. 清理Caffeine本地缓存（通过前缀匹配）
        if (localCache != null) {
            // 获取缓存中所有key
            localCache.asMap().keySet().removeIf(key ->
                    // 过滤出图片列表相关的key并删除
                    key.startsWith(CacheConstant.PICTURE_LIST_CAFFEINE_PREFIX)
            );
            log.info("Caffeine缓存清理完成，前缀：{}", CacheConstant.PICTURE_LIST_CAFFEINE_PREFIX);
        }

        // 2. 清理Redis缓存（通过前缀匹配，使用scan避免keys命令的性能问题）
        String redisPrefix = CacheConstant.PICTURE_LIST_REDIS_PREFIX;
        Set<String> keys = scanKeys(redisPrefix + "*");
        if (!keys.isEmpty()) {
            stringRedisTemplate.delete(keys);
            log.info("Redis缓存清理完成，前缀：{}，数量：{}", redisPrefix, keys.size());
        }
    }


    /**
     * 使用scan命令批量获取匹配前缀的key（替代keys命令，避免阻塞Redis）
     *
     * @param pattern 匹配模式
     */
    private Set<String> scanKeys(String pattern) {
        Set<String> keys = stringRedisTemplate.execute((RedisCallback<Set<String>>) connection -> {
            Set<String> result = new HashSet<>();
            Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions()
                    .match(pattern)
                    .count(1000) // 每次扫描1000条
                    .build());
            while (cursor.hasNext()) {
                result.add(new String(cursor.next()));
            }
            return result;
        });
        return keys;
    }


    /**
     * 从完整URL中提取COS对象的key（去掉域名部分）
     *
     * @param url     完整URL
     * @param cosHost COS的域名
     * @return COS对象的key
     */
    @Override
    public String changeUrlToCosKey(String url, String cosHost) {
        if (StrUtil.isBlank(url) || StrUtil.isBlank(cosHost)) {
            return null;
        }
        // 例如：cosHost = "https://bucket-123.cos.ap-beijing.myqcloud.com"
        if (url.startsWith(cosHost)) {
            // 截取host后的部分作为key（去掉开头的"/"）
            String key = url.substring(cosHost.length());
            return key.startsWith("/") ? key.substring(1) : key;
        }
        return null;
    }

    /**
     * 判断用户是否有操作图片权限
     *
     * @param picture   图片对象
     * @param loginUser 登录用户
     */
    @Override
    public void checkPictureAuth(Picture picture, User loginUser) {
        Long spaceId = picture.getSpaceId();
        if (spaceId == null) {
            // 公共图库
            if (!loginUser.getId().equals(picture.getUserId()) && !userService.isAdmin(loginUser)) {
                throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
            }
        } else {
            // 私人图库
            if (!loginUser.getId().equals(picture.getUserId())) {
                throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
            }
        }
    }

    /**
     * 监听空间删除事件，自动删除关联图片
     */
    @Async
    @EventListener // 监听SpaceDeletedEvent事件
    @Transactional(rollbackFor = Exception.class) // 确保删除图片在事务中执行
    public void handleSpaceDeletedEvent(SpaceDeletedEvent event) {
        Long spaceId = event.getSpaceId();
        log.info("开始删除空间[{}]关联的图片", spaceId);
        // 1. 查询该空间下的所有图片
        QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("spaceId", spaceId);
        List<Picture> pictureList = this.list(queryWrapper);
        if (CollUtil.isEmpty(pictureList)) {
            log.info("空间[{}]无关联图片，无需删除", spaceId);
            return;
        }

        // 2. 删除数据库中的图片记录
        List<Long> pictureIds = pictureList.stream()
                .map(Picture::getId)
                .collect(Collectors.toList());
        boolean removeResult = this.removeByIds(pictureIds);
        ThrowUtils.throwIf(!removeResult, ErrorCode.OPERATION_ERROR, "删除关联图片失败");

//        // 3. 异步删除COS中的图片文件（复用已有方法）
//        for (Picture picture : pictureList) {
//            this.cleanCosPicture(picture);
//        }
        log.info("空间[{}]关联的{}张图片已全部删除", spaceId, pictureList.size());
    }


}




